#include <iostream>
#include <cstdint>
/*
 * The general concept of JDK 8+'s ThreadLocalRandom's nextInt is as follows
 * Obtain a seed from the Thread t as a long, r.
 * Increment r by the predefined value GAMMA mod 2^64, and return r.
 * Pass r to the mix32 function mix32(long z)
 ***************************************************
 * Procedure mix32(long z)
 * z = (z ^ (z >> 33)) * 0xff51afd7ed558ccd mod 2^64
 * z = (z ^ (z >> 33)) * 0xc4ceb9fe1a85ec53 mod 2^64
 * return (int32)(z >> 32);
 ***************************************************
 * For the proof of concept, we use unsigned integers for consistency.
 * This can be understood as returning the high bits of a 64 bit integer.
 * Lossily removing the 32 LSB.
 * To uniquely proceed with the attack, we require 3 outputs. To obtain very few,
 * likely one, collision, we only require 2. 
 * After collecting three outputs, we place the first output into the high bits of
 * a 64 bit numebr. We then iterate through all 2^32 possible low bits by
 * concatenating them to these high bits. This allows us to get past the lossy
 * compression down to a 32 bit integer. Then, we note that both constants are
 * odd, and thus unitary on the ring Z/2^64Z, implying the existence of a
 * multiplicative inverse. We first enumerate and apply the inverse of:
 * 0xc4ceb9fe1a85ec53
 * At this point, we notice that our guess necessarily must have the 33 high bits 
 * of the previous round of output, since they are unmodified by the XOR operation.
 * Thus, since only 31 bits were affected, we can perform a XOR by a right shifted
 * value again to work our way back to the first line. Repeat this line of thought 
 * once more, and we have obtained a candidate argument into mix32.
 * Once we have reached this point, we must check our guess.
 * With an oracle, we can simply query the oracle after each guess. This allows for
 * a single-output inversion.
 * Without an oracle, we require more checks in order to find certainty.
 * In order to check, we simply add gamma to our guess and pass it to mix32. If it
 * produces the second output, we have either found our seed or one of the small
 * number of possible collisions. If we have a third output, we add 2*GAMMA to our
 * guess and pass it to mix32 to compare with it, which should practically, if not
 * completely, eliminate the chance for collision. Thus, the stream is recovered,
 * and can be fully enumerated both backwards and forwards by subtracting and 
 * adding gamma, respectively.
 *
 * Built/tested on x86_64-pc-linux-gnu (Void Linux) using GCC 14.2.1.
 */
 
const uint64_t GAMMA = 0x9e3779b97f4a7c15;
 
inline uint32_t mix32(uint64_t num)
{
 num = (num ^ (num >> 33)) * 0xff51afd7ed558ccd;
  return static_cast<uint32_t>(((num ^ (num >> 33)) * 0xc4ceb9fe1a85ec53) >> 32);
}
 
int main()
{
  uint64_t threadSeed = 0x1234567812345678;
  uint32_t T1 = mix32(threadSeed + GAMMA);
  uint32_t T2 = mix32(threadSeed + 2*GAMMA);
  uint32_t T3 = mix32(threadSeed + 3*GAMMA);
  uint64_t hi = static_cast<uint64_t>(T1) << 32;
  uint64_t recoveredSeed = 0;
 
  for (size_t i = 0; i < (1ull << 32); ++i)
  {
    uint64_t num2 = hi | static_cast<uint32_t>(i); // concat guess
    num2 *= 0x9cb4b2f8129337db; // unit inverse
    num2 = num2 ^ (num2 >> 33); // de-xor
    num2 *= 0x4f74430c22a54005; // other unit inverse
    num2 = num2 ^ (num2 >> 33); // de-xor again
    
    // double check to prevent collision
    if (mix32(num2 + GAMMA) == T2 && mix32(num2 + 2*GAMMA) == T3)
    {
      recoveredSeed = num2 - GAMMA;
      std::cout << "[+] Recovered seed." << std::endl;
      std::cout << "[*] 0x" << std::hex << recoveredSeed << std::endl;
    }
    // progress check
    if (i % 10000000 == 0)
    {
      std::cout << "[~] "
    << (static_cast<double>(i) / (1ull << 32)) * 100
    << "% Done." << std::endl;
    }
  }
  std::cout << "[+] 100.000% Done" << std::endl;
  std::cout << "Found seed: 0x" << std::hex << recoveredSeed << std::endl;
  return 0;
}
